﻿namespace Furesoft.Styling.Parser
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;

    /// <summary>
    /// Class to parse CSS text into data structures.
    /// </summary>
    internal class CssParser : TextParser
    {
        protected const string OpenComment = "/*";
        protected const string CloseComment = "*/";

        private readonly string _media;

        public CssParser(string media = null)
        {
            this._media = media;
        }

        public IEnumerable<CssParserRule> ParseAll(string css)
        {
            int start;

            this.Reset(css);
            this.StripAllComments();

            var rules = new List<CssParserRule>();

            while (!this.EndOfText)
            {
                this.MovePastWhitespace();

                if (this.Peek() == '@')
                {
                    // Process "at-rule"
                    string atRule = this.ExtractSkippedText(this.MoveToWhiteSpace).ToLower();
                    if (atRule == "@media")
                    {
                        start = this.Position;
                        this.MoveTo('{');
                        string newMedia = this.Extract(start, this.Position).Trim();

                        // Parse contents of media block
                        string innerBlock = this.ExtractSkippedText(() => this.SkipOverBlock('{', '}'));

                        // Trim curly braces
                        if (innerBlock.StartsWith("{"))
                            innerBlock = innerBlock.Remove(0, 1);
                        if (innerBlock.EndsWith("}"))
                            innerBlock = innerBlock.Substring(0, innerBlock.Length - 1);

                        // Parse CSS in block
                        var parser = new CssParser(newMedia);
                        rules.AddRange(parser.ParseAll(innerBlock));

                        continue;
                    }
                    else throw new NotSupportedException(String.Format("{0} rule is unsupported", atRule));
                }

                // Find start of next declaration block
                start = this.Position;
                this.MoveTo('{');
                if (this.EndOfText) // Done if no more
                    break;

                // Parse selectors
                string selectors = this.Extract(start, this.Position);
                var rule = new CssParserRule(this._media)
                               {
                                   Selectors = from s in selectors.Split(',')
                                               let s2 = s.Trim()
                                               where s2.Length > 0
                                               select s2
                               };

                // Parse declarations
                this.MoveAhead();
                start = this.Position;
                this.MoveTo('}');
                string properties = this.Extract(start, this.Position);
                rule.Declarations = from s in properties.Split(Convert.ToChar(";")) 
                                    let s2 = s.Trim()
                                    where s2.Length > 0
                                    let x = s2.IndexOf(':')
                                    select new CssParserDeclaration
                                               {
                                                   Property = s2.Substring(0, (x < 0) ? 0 : x).TrimEnd(),
                                                   Value = s2.Substring((x < 0) ? 0 : x + 1).TrimStart()
                                               };

                // Skip over closing curly brace
                this.MoveAhead();

                // Add rule to results
                rules.Add(rule);
            }
            // Return rules to caller
            return rules;
        }

        /// <summary>
        /// Removes all comments from the current text.
        /// </summary>
        protected void StripAllComments()
        {
            var sb = new StringBuilder();

            this.Reset();
            while (!this.EndOfText)
            {
                if (this.IsComment())
                {
                    this.SkipOverComment();
                }
                else if (this.IsQuote())
                {
                    sb.Append(this.ExtractSkippedText(this.SkipOverQuote));
                }
                else
                {
                    sb.Append(this.Peek());
                    this.MoveAhead();
                }
            }
            this.Reset(sb.ToString());
        }

        /// <summary>
        /// Moves to the next occurrence of the specified character, skipping
        /// over quoted values.
        /// </summary>
        /// <param name="c">Character to find</param>
        public new void MoveTo(char c)
        {
            while (this.Peek() != c && !this.EndOfText)
            {
                if (this.IsQuote())
                    this.SkipOverQuote();
                else
                    this.MoveAhead();
            }
        }

        /// <summary>
        /// Moves to the next whitespace character.
        /// </summary>
        private void MoveToWhiteSpace()
        {
            while (!Char.IsWhiteSpace(this.Peek()) && !this.EndOfText)
                this.MoveAhead();
        }

        /// <summary>
        /// Skips over the quoted text that starts at the current position.
        /// </summary>
        protected void SkipOverQuote()
        {
            Debug.Assert(this.IsQuote());
            char quote = this.Peek();
            this.MoveAhead();
            while (this.Peek() != quote && !this.EndOfText)
                this.MoveAhead();
            this.MoveAhead();
        }

        /// <summary>
        /// Skips over the comment that starts at the current position.
        /// </summary>
        protected void SkipOverComment()
        {
            Debug.Assert(this.IsComment());
            this.MoveAhead(OpenComment.Length);
            this.MoveTo(CloseComment, true);
            this.MoveAhead(CloseComment.Length);
        }

        /// <summary>
        /// Skips over a block of text bounded by the specified start and end
        /// character. Blocks may be nested, in which case the endChar of
        /// inner blocks is ignored (the entire outer block is returned).
        /// Sets the current position to just after the final end character.
        /// </summary>
        /// <param name="startChar"></param>
        /// <param name="endChar"></param>
        private void SkipOverBlock(char startChar, char endChar)
        {
            Debug.Assert(this.Peek() == startChar);
            this.MoveAhead();
            int depth = 1;
            while (depth > 0 && !this.EndOfText)
            {
                if (this.IsQuote())
                {
                    this.SkipOverQuote();
                }
                else
                {
                    if (this.Peek() == startChar)
                        depth++;
                    else if (this.Peek() == endChar)
                        depth--;
                    this.MoveAhead();
                }
            }
        }

        /// <summary>
        /// Calls the specified action and then returns a string of all characters
        /// that the method skipped over.
        /// </summary>
        /// <param name="a">Action to call</param>
        /// <returns></returns>
        protected string ExtractSkippedText(Action a)
        {
            int start = this.Position;
            a();
            return this.Extract(start, this.Position);
        }

        /// <summary>
        /// Indicates if single or double-quoted text begins at the current
        /// location.
        /// </summary>
        protected bool IsQuote()
        {
            return (this.Peek() == '\'' || this.Peek() == '"');
        }

        /// <summary>
        /// Indicates if a comment begins at the current location.
        /// </summary>
        protected bool IsComment()
        {
            return this.IsEqualTo(OpenComment);
        }

        /// <summary>
        /// Determines if text at the current position matches the specified string.
        /// </summary>
        /// <param name="s">String to compare against current position</param>
        protected bool IsEqualTo(string s)
        {
            Debug.Assert(!String.IsNullOrEmpty(s));
            for (int i = 0; i < s.Length; i++)
            {
                if (this.Peek(i) != s[i])
                    return false;
            }
            return true;
        }
    }
}